package com.longtxt.powerband

import android.graphics.BitmapFactory
import com.google.gson.Gson
import com.jieli.bmp_convert.BmpConvert
import com.jieli.bmp_convert.OnConvertListener
import com.jieli.jl_fatfs.FatFsErrCode
import com.jieli.jl_fatfs.interfaces.OnFatFileProgressListener
import com.jieli.jl_fatfs.model.FatFile
import com.jieli.jl_fatfs.utils.FatUtil
import com.jieli.jl_rcsp.constant.JLChipFlag
import com.jieli.jl_rcsp.interfaces.watch.OnWatchOpCallback
import com.jieli.jl_rcsp.model.base.BaseError
import com.longtxt.powerband.core.BLEConstants
import com.longtxt.powerband.core.BlePeripheral
import com.longtxt.powerband.jieLi.JLWatchManager
import com.longtxt.powerband.models.BaseResponse
import com.longtxt.powerband.models.CustomThemeData
import com.longtxt.powerband.util.BitmapUtil
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException
import java.nio.ByteBuffer

class WatchThemeManager(
    private val baseFileUrl: File,
    private val blePeripheral: BlePeripheral,
    private val isRound: Boolean,
    private val isJLieWatch: Boolean,
    private val jlCustomSuffix: String,
    private val jlDialMarketSuffix: String
) {
    private val mBmpConvert = BmpConvert()
    private val gsonInstance = Gson()
    private var imgLocalPath = ""
    private val callBackStack = mutableMapOf<String, BleResponseCallback>()
    private var watchThemeProgressCallback: ((Int) -> Unit)? = null

    private val mMtu = 180

    private var fileByte = ByteArray(0)
    private var fileSize = 0
    private var dataAddress = 0


    fun addNewWatchTheme(filePath: String, progress: (Int) -> Unit, callback: (String) -> Unit) {
        watchThemeProgressCallback = progress
        callBackStack["addNewWatchTheme"] = callback
        preProcessFile(filePath)
    }

    fun addWatchBackground(
        customThemeData: CustomThemeData,
        progress: (Int) -> Unit,
        callback: (String) -> Unit
    ) {
        watchThemeProgressCallback = progress
        callBackStack["addWatchBackground"] = callback
        preProcessImg(customThemeData)
    }


    private fun callWatchThemeFailed(msg: String, callBackName: String) {
        val baseResponse = BaseResponse<String>(
            code = 0,
            message = msg,
            data = "Failed"
        )
        callBackStack[callBackName]?.let {
            it(gsonInstance.toJson(baseResponse))
        }
    }

    private fun callWatchThemeSuccess(callBackName: String) {
        val baseResponse = BaseResponse<String>(
            code = 1,
            message = "Success",
            data = "Success"
        )
        callBackStack[callBackName]?.let {
            it(gsonInstance.toJson(baseResponse))
        }
    }

    private fun handleJLWatch(localFile: File) {
        val copyToPath =
            "${baseFileUrl}/watch/$jlDialMarketSuffix"
        if (File(copyToPath).exists()) {
            File(copyToPath).delete()
        }
        localFile.copyTo(File(copyToPath))
        JLWatchManager.shared.createWatchFile(copyToPath, true, object : OnFatFileProgressListener {
            override fun onStart(p0: String?) {

            }

            override fun onProgress(p0: Float) {
                watchThemeProgressCallback?.invoke(p0.toInt())
            }

            override fun onStop(p0: Int) {
                if (p0 == 0) {
                    switchToNewDial(copyToPath)
                } else {
                    callWatchThemeFailed("Failed to create watch file", "addNewWatchTheme")
                }
            }

        })
    }

    private fun preProcessFile(filePath: String) {
        if (filePath.isEmpty()) {
            callWatchThemeFailed("File path is empty", "addNewWatchTheme")
            return
        }
        val suffix = filePath.substringAfterLast(".").lowercase()
        if (suffix != "bin") {
            callWatchThemeFailed("Invalid file format", "addNewWatchTheme")
            return
        }
        val fileTarget = File(filePath)
        if (fileTarget.exists()) {
            val data = readBingFile(fileTarget)
            if (data == null || data.isEmpty()) {
                callWatchThemeFailed("Failed to read file", "addNewWatchTheme")
                return
            }
            fileByte = data
            fileSize = fileByte.size
            dataAddress = 0
            startFileRequest(fileTarget)
        } else {
            callWatchThemeFailed("File not found", "addNewWatchTheme")
        }
    }

    private fun preProcessImg(customThemeData: CustomThemeData) {
        if (customThemeData.imagePath.isEmpty()) {
            callWatchThemeFailed("File path is empty", "addWatchBackground")
            return
        }
        val suffix = customThemeData.imagePath.substringAfterLast(".").lowercase()
        if (suffix != "png" && suffix != "jpg") {
            callWatchThemeFailed("Invalid file format", "addWatchBackground")
            return
        }
        val tempFile = File(customThemeData.imagePath)
        if (!tempFile.exists()|| tempFile.length() == 0L) {
            callWatchThemeFailed("File not found", "addWatchBackground")
            return
        }
        val fileName = customThemeData.imagePath.substringAfterLast("/")
        imgLocalPath = "${baseFileUrl}/watch/img/$fileName"
        val fileTarget = File(imgLocalPath)
        val outDir = fileTarget.parentFile
        if (outDir != null && !outDir.exists()) {
            if (!outDir.mkdirs()) {
                callWatchThemeFailed("Failed to create directory", "addWatchBackground")
                return
            }
        }
        if (!fileTarget.exists()) {
            tempFile.copyTo(fileTarget)
        }
        if (isJLieWatch) {
            MainScope().launch {
                sendWatchBgParameter(customThemeData)
                delay(2000)
                imageResizeForJL()
            }
        } else {
            MainScope().launch {
                readImageInfo()
                val lenByte = byteArrayOf(0x01) + intToByteArray(fileSize)
                classicWatchBgSignal(lenByte)
                delay(500)
                sendWatchBgParameter(customThemeData)
                delay(3000)
                sendFilePacket(true)
            }
        }
    }

    private fun readImageInfo() {
        val file = File(imgLocalPath)
        val data = readBingFile(file)
        if (data == null) {
            callWatchThemeFailed("Failed to read file", "addWatchBackground")
            return
        }
        val originalBitmap = BitmapFactory.decodeFile(imgLocalPath)
        fileByte = BitmapUtil.bitmap2RGB565(originalBitmap)
        fileSize = fileByte.size
        dataAddress = 0
    }

    private fun startFileRequest(fileTarget: File) {
        if (isJLieWatch) {
            handleJLWatch(fileTarget)
        } else {
            handleClassicWatch()
        }
    }

    private fun intToByteArray(value: Int): ByteArray {
        val bytes = ByteArray(8)
        ByteBuffer.wrap(bytes).putInt(value)
        return bytes.copyOfRange(0, 4).reversed().toByteArray()
    }

    private fun sendWatchBgParameter(customThemeData: CustomThemeData) {
        blePeripheral.writeJsonRequest(
            byteArrayOf(
                BLEConstants.COMMAND_SCREEN_PARAM,
                customThemeData.datePosition.toByte(),
                customThemeData.contentTop.toByte(),
                customThemeData.contentBottom.toByte(),
                customThemeData.color.toByte()
            )
        )
    }

    private fun classicWatchBgSignal(type: ByteArray) {
        blePeripheral.writeJsonRequest(
            byteArrayOf(
                BLEConstants.COMMAND_SCREEN_START,
                *type
            )
        )
    }

    private fun classicWatchThemeSignal(type: ByteArray) {
        blePeripheral.writeJsonRequest(
            byteArrayOf(
                BLEConstants.COMMAND_MARKET_START,
                *type
            )
        )
    }

    private fun handleClassicWatch() {
        MainScope().launch {
            val lenByte = byteArrayOf(0x01) + intToByteArray(fileSize)
            classicWatchThemeSignal(lenByte)
            delay(3000)
            sendFilePacket(false)
        }
    }

    // Call notify from outside of this class
    fun sendFilePacket(isBgData: Boolean) {
        if (dataAddress >= fileSize) {
            fileSendCompleted(isBgData)
            return
        }
        val toIndex = if (dataAddress + mMtu >= fileSize) fileSize else dataAddress + mMtu
        val packet = fileByte.copyOfRange(dataAddress, toIndex)
        dataAddress = toIndex
        val progress = ((dataAddress.toDouble() / fileSize.toDouble()) * 100).toInt()
        watchThemeProgressCallback?.invoke(progress)
        if (isBgData) {
            sendBackgroundData(packet)
        } else {
            sendThemeData(packet)
        }
    }

    private fun sendThemeData(data: ByteArray) {
        blePeripheral.writeJsonRequest(
            byteArrayOf(
                BLEConstants.COMMAND_MARKET_DATA,
                *data
            )
        )

    }

    private fun sendBackgroundData(data: ByteArray) {
        blePeripheral.writeJsonRequest(
            byteArrayOf(
                BLEConstants.COMMAND_SCREEN_DATA,
                *data
            )
        )
    }

    private fun fileSendCompleted(isBgData: Boolean) {
        if (isBgData) {
            classicWatchBgSignal(byteArrayOf(0x02))
        } else {
            classicWatchThemeSignal(byteArrayOf(0x02))
        }
        val callName = if (isBgData) "addWatchBackground" else "addNewWatchTheme"
        callWatchThemeSuccess(callName)
    }


    private fun switchToNewDial(path: String) {
        JLWatchManager.shared.setCurrentWatchInfo(
            FatUtil.getFatFilePath(path),
            object : OnWatchOpCallback<FatFile> {
                override fun onSuccess(p0: FatFile?) {
                    callWatchThemeSuccess("addNewWatchTheme")
                }

                override fun onFailed(p0: BaseError?) {
                    callWatchThemeFailed("Failed to switch to new dial", "addNewWatchTheme")
                }
            })
    }



    private fun readBingFile(f: File): ByteArray? {
        try {
            val inputStream = f.inputStream()
            val len = inputStream.available()
            val buf = ByteArray(len)
            val byteArrayOutputStream = ByteArrayOutputStream()
            var n: Int
            while (-1 != inputStream.read(buf).also { n = it }) {
                byteArrayOutputStream.write(buf, 0, n)
            }
            return byteArrayOutputStream.toByteArray()
        } catch (e: IOException) {
            e.printStackTrace()
        }
        return null
    }


    private fun imageResizeForJL() {
        val device = JLWatchManager.shared.deviceInfo
        val isBr28 = device.sdkType == JLChipFlag.JL_CHIP_FLAG_701X_WATCH
        var type = if (isBr28) BmpConvert.TYPE_BR_28 else BmpConvert.TYPE_BR_23
        val watchConfig = JLWatchManager.shared.readWatchConfig()
        if (isBr28 && watchConfig.functionOption.isSupportDialExpandInfo) {
            type = BmpConvert.TYPE_BR_28_ALPHA
        }

        val customFolder = "${baseFileUrl}/watch/custom"
        val customFolderFile = File(customFolder)
        if (!customFolderFile.exists()) {
            customFolderFile.mkdirs()
        }

        val srcBmp = BitmapFactory.decodeFile(imgLocalPath)
        val cropBitmap = BitmapUtil.getCropBitmap(watchConfig.dialExpandInfo, srcBmp)
        val cropPath = "${baseFileUrl}/watch/custom/cut_crop.png"
        BitmapUtil.bitmapToFile(cropBitmap, cropPath, 100)

        val fillBitmap = BitmapUtil.getFillBitmap(watchConfig.dialExpandInfo, cropBitmap)
        val fillPath = "${baseFileUrl}/watch/custom/cut_fill.png"
        BitmapUtil.bitmapToFile(fillBitmap, fillPath, 100)

        cropBitmap?.recycle()
        fillBitmap?.recycle()
        val outputPosition = jlCustomSuffix
        val outPutName = outputPosition.replace("WATCH", "BGP_W00")
        val outputPath = "${baseFileUrl}/watch/custom/$outPutName"
        mBmpConvert.bitmapConvert(
            type,
            cropPath,
            outputPath,
            object : OnConvertListener {
                override fun onStart(p0: String?) {}

                override fun onStop(p0: Boolean, p1: String?) {
                    sendCustomDial(outputPath)
                }
            })

    }

    private fun sendCustomDial(dialPath: String) {
        JLWatchManager.shared.createWatchFile(
            dialPath,
            true,
            object : OnFatFileProgressListener {
                override fun onStart(p0: String?) {
                }

                override fun onProgress(p0: Float) {
                    watchThemeProgressCallback?.invoke(p0.toInt())
                }

                override fun onStop(p0: Int) {
                    onCustomDialStop(p0)
                }

            })
    }

    private fun onCustomDialStop(result: Int) {
        if (result == FatFsErrCode.RES_OK) {
            val outputPosition = jlCustomSuffix
            JLWatchManager.shared.setCurrentWatchInfo(
                "/$outputPosition",
                object : OnWatchOpCallback<FatFile> {
                    override fun onSuccess(p0: FatFile?) {
                        switchToCustomBg()
                    }

                    override fun onFailed(p0: BaseError?) {
                        callWatchThemeFailed(
                            "Failed to switch to custom dial",
                            "addWatchBackground"
                        )
                    }

                })

        } else {
            callWatchThemeFailed("Failed to create custom dial", "addWatchBackground")
        }
    }

    private fun switchToCustomBg() {
        val outputPosition = jlCustomSuffix
        val customBgPath = outputPosition.replace("WATCH", "BGP_W00")
        JLWatchManager.shared.enableCustomWatchBg(
            "/$customBgPath",
            object : OnWatchOpCallback<FatFile> {
                override fun onSuccess(p0: FatFile?) {
                    callWatchThemeSuccess("addWatchBackground")
                }

                override fun onFailed(p0: BaseError) {
                    callWatchThemeFailed(
                        "Failed to switch to custom dial ${p0.message}",
                        "addWatchBackground"
                    )
                }

            })
    }
}